﻿using System;
using System.Collections;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Threading;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Threading;
using Foundation.MarkupExtensions;
using Timer = System.Timers.Timer;

namespace Foundation.ExtensionMethods
{
    /// <summary>
    /// CommonExtensions.
    /// </summary>
    public static class CommonExtensions
    {
        public static RegexOptions InvertFlags(this RegexOptions value, RegexOptions flags)
        {
            if (flags.HasFlag(RegexOptions.ECMAScript))
            {
                if (value.HasFlag(RegexOptions.Singleline))
                    value = value & ~RegexOptions.Singleline;
                if (value.HasFlag(RegexOptions.ExplicitCapture))
                    value = value & ~RegexOptions.ExplicitCapture;
                if (value.HasFlag(RegexOptions.IgnorePatternWhitespace))
                    value = value & ~RegexOptions.IgnorePatternWhitespace;
                if (value.HasFlag(RegexOptions.RightToLeft))
                    value = value & ~RegexOptions.RightToLeft;
                if (value.HasFlag(RegexOptions.CultureInvariant))
                    value = value & ~RegexOptions.CultureInvariant;
            }

            if (flags.HasFlag(RegexOptions.Singleline)
                || flags.HasFlag(RegexOptions.ExplicitCapture)
                || flags.HasFlag(RegexOptions.IgnorePatternWhitespace)
                || flags.HasFlag(RegexOptions.RightToLeft)
                || flags.HasFlag(RegexOptions.CultureInvariant))
            {
                value = value & ~RegexOptions.ECMAScript;
            }

            return value ^ flags;
        }

        public static Enum InvertFlag(this Enum value, Enum flag)
        {
            if (!value.GetType().IsEquivalentTo(flag.GetType()))
            {
                throw new ArgumentException("Argument_EnumTypeDoesNotMatch");
            }

            var v = (ulong)((object)value);
            var f = (ulong)((object)flag);
            v = v ^ f;
            return (Enum)Enum.ToObject(flag.GetType(), v);
        }

        public static Enum SetFlag(this Enum value, Enum flag)
        {
            if (!value.GetType().IsEquivalentTo(flag.GetType()))
            {
                throw new ArgumentException("Argument_EnumTypeDoesNotMatch");
            }

            var v = (ulong) ((object)value);
            var f = (ulong) ((object)flag);
            v = v | f;
            return (Enum) Enum.ToObject(flag.GetType(), v);
        }

        public static Enum ResetFlag(this Enum value, Enum flag)
        {
            if (!value.GetType().IsEquivalentTo(flag.GetType()))
            {
                throw new ArgumentException("Argument_EnumTypeDoesNotMatch");
            }

            var v = (ulong)((object)value);
            var f = (ulong)((object)flag);
            v = v & ~f;
            return (Enum)Enum.ToObject(flag.GetType(), v);
        }

        /// <summary>
        /// Sets the localizing.
        /// </summary>
        /// <param name="dependencyObject">The dependency object.</param>
        /// <param name="dependencyProperty">The dependency property.</param>
        /// <param name="localizing">The localizing.</param>
        public static void SetLocalizing(this DependencyObject dependencyObject,
                                         DependencyProperty dependencyProperty, Localizing localizing)
        {
            dependencyObject.SetValue(
                dependencyProperty, localizing.ProvideValue(new SelfProvider(dependencyObject, dependencyProperty)));
        }

        /// <summary>
        /// Begins the invoke.
        /// </summary>
        /// <param name="dispatcher">The dispatcher.</param>
        /// <param name="action">The action.</param>
        public static void BeginInvoke(this Dispatcher dispatcher, Action action)
        {
            dispatcher.BeginInvoke((ThreadStart) (() => action()));
        }

        /// <summary>
        /// Invokes the specified dispatcher.
        /// </summary>
        /// <param name="dispatcher">The dispatcher.</param>
        /// <param name="action">The action.</param>
        public static void Invoke(this Dispatcher dispatcher, Action action)
        {
            dispatcher.Invoke((ThreadStart)(() => action()));
        }

        /// <summary>
        /// Begins the invoke.
        /// </summary>
        /// <param name="dispatcher">The dispatcher.</param>
        /// <param name="action">The action.</param>
        /// <param name="parameter">The parameter.</param>
        public static void BeginInvoke(this Dispatcher dispatcher, Action<object> action, object parameter)
        {
            dispatcher.BeginInvoke((ThreadStart) (() => action(parameter)));
        }

        /// <summary>
        /// Clones the specified source.
        /// </summary>
        /// <typeparam name="TItem">The type of the item.</typeparam>
        /// <param name="source">The source.</param>
        /// <returns></returns>
        public static TItem Clone<TItem>(this TItem source)
        {
            var type = source.GetType();
            var clone = Activator.CreateInstance<TItem>();
            type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
                .Where(p => p.CanWrite)
                .ForEach(p =>
                    {
                        var value = p.GetValue(source);
                        if (value == null) return;
                        p.SetValue(clone, value, BindingFlags.Public, null, null, null);
                    });

            return clone;
        }

        /// <summary>
        /// Gets the root menu placement.
        /// </summary>
        /// <param name="menuItem">The menu item.</param>
        /// <returns></returns>
        public static DependencyObject GetRootMenuPlacement(this MenuItem menuItem)
        {
            if (menuItem == null) return null;

            var presenter = menuItem.GetNearestVisualAncestor<ItemsPresenter>();
            while (presenter.TemplatedParent.GetNearestVisualAncestor<ItemsPresenter>() != null)
            {
                presenter = presenter.TemplatedParent.GetNearestVisualAncestor<ItemsPresenter>();
            }

            var rootMenu = presenter.TemplatedParent;
            var contextMenu = rootMenu as ContextMenu;
            var placementTarget =
                contextMenu == null
                    ? rootMenu.GetNearestLogicalAncestor<DependencyObject>()
                    : contextMenu.PlacementTarget;

            return placementTarget;
        }

        /// <summary>
        /// Delays the call.
        /// </summary>
        /// <param name="o">The o.</param>
        /// <param name="delay">The delay in milliseconds.</param>
        /// <param name="method">The method.</param>
        /// <returns></returns>
        public static Timer DelayCall(this object o, double delay, Action method)
        {
            var timer = new Timer(delay);
            timer.Elapsed +=
                (sender, args) =>
                    {
                        timer.Dispose();
                        Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Normal, method);
                    };

            timer.Start();
            return timer;
        }

        /// <summary>
        /// Updates the binding targets by logical tree.
        /// </summary>
        /// <param name="item">The item.</param>
        public static void UpdateBindingTargetsByLogicalTree(this FrameworkElement item)
        {
            UpdateBindingTargets(item);
            foreach (var child in item.GetLogicalChildren().Where(child => child as FrameworkElement != null))
                UpdateBindingTargetsByVisualTree((FrameworkElement) child);
        }

        /// <summary>
        /// Updates the binding targets by visual tree.
        /// </summary>
        /// <param name="item">The item.</param>
        public static void UpdateBindingTargetsByVisualTree(this FrameworkElement item)
        {
            UpdateBindingTargets(item);
            foreach (var child in item.GetVisualChildren().Where(child => child as FrameworkElement != null))
                UpdateBindingTargetsByVisualTree((FrameworkElement)child);
        }

        /// <summary>
        /// Updates the binding targets.
        /// </summary>
        /// <param name="item">The item.</param>
        public static void UpdateBindingTargets(this FrameworkElement item)
        {
            var infos =
                item.GetType().
                    GetFields(BindingFlags.Public | BindingFlags.FlattenHierarchy |
                              BindingFlags.Instance | BindingFlags.Static);

            foreach (var field in infos)
            {
                if (field.FieldType != typeof (DependencyProperty)) continue;
                var dependencyProperty = (DependencyProperty) field.GetValue(null);
                var bindingexpression = BindingOperations.GetBindingExpression(item, dependencyProperty);
                var multiBindingExpression = BindingOperations.GetMultiBindingExpression(item, dependencyProperty);
                var priorityBindingExpression = BindingOperations.GetPriorityBindingExpression(item, dependencyProperty);

                if (bindingexpression != null)
                    bindingexpression.UpdateTarget();

                if (multiBindingExpression != null)
                    multiBindingExpression.UpdateTarget();

                if (priorityBindingExpression != null)
                    priorityBindingExpression.UpdateTarget();
            }
        }

        /// <summary>
        /// Equalses the by properties.
        /// </summary>
        /// <param name="x">The x.</param>
        /// <param name="o">The o.</param>
        /// <param name="maxLevel">The max level.</param>
        /// <param name="ignoredProperties">The ignored properties.</param>
        /// <returns></returns>
        public static bool EqualsByProperties(this object x, object o, int maxLevel = 0, string[] ignoredProperties = null)
        {
            if (Equals(x, o) || (x == null && o == null))
                return true;

            if (x == null || o == null)
                return false;

            if (x is ValueType && o is ValueType)
                return Equals(x, o);

            if (x is IEnumerable || maxLevel < 0)
                return true;

            var properties = x.GetType().GetProperties().ToList();
            if (ignoredProperties != null)
                properties = properties.Where(p => !ignoredProperties.Contains(p.Name)).ToList();
            return properties.All(p => p.GetValue(x).EqualsByProperties(p.GetValue(o), maxLevel - 1, ignoredProperties));
        }

        /// <summary>
        /// Gets the value.
        /// </summary>
        /// <param name="property">The property.</param>
        /// <param name="obj">The obj.</param>
        /// <param name="bindingFlags">The binding flags.</param>
        /// <returns></returns>
        public static object GetValue(this PropertyInfo property, object obj, BindingFlags bindingFlags = BindingFlags.Public)
        {
            return property.GetValue(obj, bindingFlags, null, null, null);
        }

        /// <summary>
        /// Gets the specified obj.
        /// </summary>
        /// <param name="obj">The obj.</param>
        /// <param name="path">Name of the property.</param>
        /// <returns></returns>
        public static object GetPropertyValue(this object obj, string path)
        {
            var properties = path.Split('.');
            foreach (var propertyName in properties)
            {
                var propertyInfo = obj.GetType().GetProperty(propertyName);
                var value = propertyInfo.GetValue(obj);
                obj = value;
            }

            return obj;
        }

        /// <summary>
        /// Disposes if not null.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="obj">The obj.</param>
        /// <returns></returns>
        public static bool SafeDispose<T>(this T obj) where T: class, IDisposable
        {
            if (obj != null)
            {
                obj.Dispose();
                return true;
            }

            return false;
        }
    }
}
